﻿using Dapper.ExpressionExtension.Resolvers;
using Dapper.ExpressionExtension.SqlBuilders;
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Dapper.ExpressionExtension.ExpressionMappers
{
    /// <summary>
    /// sql表达式
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public class SqlExpression<TEntity>
    {
        private static readonly Type EntityType = typeof(TEntity);
        private readonly StringBuilder _setBuilder = new StringBuilder();
        private readonly StringBuilder _whereBuilder = new StringBuilder();
        private readonly StringBuilder _orderByBuilder = new StringBuilder();
        private readonly DynamicParameters _parameters = new DynamicParameters();
        private string _selectQuery;
        private string _pagingQuery;
        private int _parameterIndex;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sqlBuilder"></param>
        public SqlExpression(ISqlBuilder sqlBuilder)
        {
            SqlBuilder = sqlBuilder;
        }

        /// <summary>
        /// 
        /// </summary>
        protected virtual ISqlBuilder SqlBuilder { get; }

        /// <summary>
        /// 查询所有字段
        /// </summary>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> Select()
        {
            _selectQuery = $"select * from {ResolverHelper.Table(typeof(TEntity), SqlBuilder)}";
            return this;
        }

        /// <summary>
        /// 查询指定字段
        /// </summary>
        /// <param name="selector"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> Select(Expression<Func<TEntity, TEntity>> selector)
        {
            if (selector == null)
            {
                throw new ArgumentNullException(nameof(selector));
            }
            var column = VisitExpression(selector) as string;

            // Create the select query
            var tableName = ResolverHelper.Table(EntityType, SqlBuilder);
            _selectQuery = $"select {column} from {tableName}";
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TMember"></typeparam>
        /// <param name="condition"></param>
        /// <param name="expression"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> SetIf<TMember>(bool condition, Expression<Func<TEntity, TMember>> expression, TMember value)
        {
            return condition ? Set(expression, value) : this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TMember"></typeparam>
        /// <param name="expression"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> Set<TMember>(Expression<Func<TEntity, TMember>> expression, TMember value)
        {
            if (expression == null)
            {
                return this;
            }

            AddParameter(value, out var paramName);
            AppendToSet(_setBuilder.Length == 0 ? null : ",", expression.Body, paramName);
            return this;
        }

        private void AppendToSet(string conditionOperator, Expression expression, string paramName)
        {
            var sqlExpression = $"{VisitExpression(expression)} = {paramName}";
            if (_setBuilder.Length == 0)
            {
                _setBuilder.Append($" update {ResolverHelper.Table(typeof(TEntity), SqlBuilder)} set ");
            }
            else
            {
                _setBuilder.AppendFormat(" {0} ", conditionOperator);
            }

            _setBuilder.Append(sqlExpression);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> Where(Expression<Func<TEntity, bool>> expression)
        {
            if (expression == null)
            {
                return this;
            }

            AppendToWhere(_whereBuilder.Length == 0 ? null : "and", expression);

            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="condition"></param>
        /// <param name="expression"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> AndWhereIf(bool condition, Expression<Func<TEntity, bool>> expression)
        {
            return condition ? AndWhere(expression) : this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> AndWhere(Expression<Func<TEntity, bool>> expression)
        {
            if (_whereBuilder.Length == 0)
            {
                throw new InvalidOperationException("Start the where statement with the 'Where' method.");
            }
            if (expression != null)
            {
                AppendToWhere("and", expression);
            }
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> OrWhere(Expression<Func<TEntity, bool>> expression)
        {
            if (_whereBuilder.Length == 0)
            {
                throw new InvalidOperationException("Start the where statement with the 'Where' method.");
            }
            if (expression != null)
            {
                AppendToWhere("or", expression);
            }
            return this;
        }

        private void AppendToWhere(string conditionOperator, Expression expression)
        {
            var sqlExpression = VisitExpression(expression).ToString();
            if (_whereBuilder.Length == 0)
            {
                _whereBuilder.Append(" where ");
            }
            else
            {
                _whereBuilder.AppendFormat(" {0} ", conditionOperator);
            }

            _whereBuilder.Append("(" + sqlExpression + ")");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pageNumber"></param>
        /// <param name="pageSize"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> Page(int pageNumber, int pageSize)
        {
            _pagingQuery = SqlBuilder.BuildPaging(null, pageNumber, pageSize)[1..];
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="orderBy"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> OrderBy(Expression<Func<TEntity, object>> orderBy)
        {
            OrderByCore(orderBy, "asc");
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="orderBy"></param>
        /// <returns></returns>
        public virtual SqlExpression<TEntity> OrderByDescending(Expression<Func<TEntity, object>> orderBy)
        {
            OrderByCore(orderBy, "desc");
            return this;
        }

        private void OrderByCore(Expression<Func<TEntity, object>> selector, string direction)
        {
            if (selector == null)
            {
                throw new ArgumentNullException(nameof(selector));
            }

            var column = VisitExpression(selector.Body) as string;
            AppendOrderBy(column, direction);
        }

        private void AppendOrderBy(string column, string direction)
        {
            if (string.IsNullOrEmpty(column))
            {
                throw new ArgumentNullException(nameof(column));
            }
            if (string.IsNullOrEmpty(direction))
            {
                throw new ArgumentNullException(nameof(direction));
            }
            if (_orderByBuilder.Length == 0)
            {
                _orderByBuilder.Append($" order by {column} {direction}");
            }
            else
            {
                _orderByBuilder.Append($", {column} {direction}");
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitExpression(Expression expression)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.Lambda:
                    return VisitLambda((LambdaExpression)expression);

                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    return VisitBinary((BinaryExpression)expression);

                case ExpressionType.Convert:
                case ExpressionType.Not:
                    return VisitUnary((UnaryExpression)expression);

                case ExpressionType.New:
                    throw new NotSupportedException($"ExpressionType.New is not supported");

                case ExpressionType.MemberAccess:
                    return VisitMemberAccess((MemberExpression)expression);
                case ExpressionType.MemberInit:
                    return VisitMemberInit((MemberInitExpression)expression);

                case ExpressionType.Constant:
                    return VisitConstantExpression((ConstantExpression)expression);
                case ExpressionType.Call:
                    return VisitCallExpression((MethodCallExpression)expression);
                case ExpressionType.Invoke:
                    return VisitExpression(((InvocationExpression)expression).Expression);
            }

            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        protected enum TextSearch
        {
            /// <summary>
            /// like '%xxx%'
            /// </summary>
            Contains,

            /// <summary>
            /// like 'xxx%'
            /// </summary>
            StartsWith,

            /// <summary>
            /// like '%xxx'
            /// </summary>
            EndsWith
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitCallExpression(MethodCallExpression expression)
        {
            var method = expression.Method.Name.ToLower();
            switch (method)
            {
                case "contains":
                    //区分文本的包含还是list的包含
                    if (expression.Object != null && expression.Object.Type == typeof(string))
                    {
                        return VisitContainsExpression(expression, TextSearch.Contains);
                    }
                    else
                    {
                        return VisitInExpression(expression);
                    }
                case "startswith":
                    return VisitContainsExpression(expression, TextSearch.StartsWith);
                case "endswith":
                    return VisitContainsExpression(expression, TextSearch.EndsWith);
            }

            return expression;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitInExpression(MethodCallExpression expression)
        {
            Expression collection;
            Expression property;
            if (expression.Object == null && expression.Arguments.Count == 2)
            {
                //如果是静态方法，且有2个参数，则调用方来源是System.Linq.Enumerable
                collection = expression.Arguments[0];
                property = expression.Arguments[1];
            }
            else if (expression.Object != null && expression.Arguments.Count == 1)
            {
                //如果不是静态方法，且只有1个参数，则调用方来源是System.Collections.IList
                collection = expression.Object;
                property = expression.Arguments[0];
            }
            else
            {
                throw new NotSupportedException("Unsupported method call: " + expression.Method.Name);
            }

            var inClause = new StringBuilder("(");
            foreach (var value in (System.Collections.IEnumerable)VisitMemberAccess((MemberExpression)collection))
            {
                AddParameter(value, out var paramName);
                inClause.Append($"{paramName},");
            }
            if (inClause.Length == 1)
            {
                inClause.Append("null,");
            }
            inClause[^1] = ')';

            return $"{VisitExpression(property)} in {inClause}";
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <param name="textSearch"></param>
        /// <returns></returns>
        protected virtual object VisitContainsExpression(MethodCallExpression expression, TextSearch textSearch)
        {
            var column = VisitExpression(expression.Object);
            if (expression.Arguments.Count == 0 || expression.Arguments.Count > 1)
            {
                throw new ArgumentException("Contains-expression should contain exactly one argument.", nameof(expression));
            }

            var value = VisitExpression(expression.Arguments[0]);
            var textLike = textSearch switch
            {
                TextSearch.Contains => $"%{value}%",
                TextSearch.StartsWith => $"{value}%",
                TextSearch.EndsWith => $"%{value}",
                _ => throw new ArgumentOutOfRangeException($"Invalid TextSearch value '{textSearch}'.", nameof(textSearch)),
            };
            AddParameter(textLike, out var paramName);

            return $"{column} like {paramName}";
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitLambda(LambdaExpression expression)
        {
            if (expression.Body.NodeType == ExpressionType.MemberAccess)
            {
                var member = expression.Body as MemberExpression;
                if (member?.Expression != null)
                {
                    return $"{VisitMemberAccess(member)} = '1'";
                }
            }

            return VisitExpression(expression.Body);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitBinary(BinaryExpression expression)
        {
            object left, right;
            var operand = GetOperand(expression.NodeType);
            if (operand == "and" || operand == "or")
            {
                // Process left and right side of the "and/or" expression:
                // Foo == 42    or      Bar == 42
                //   left    operand     right
                //
                if (expression.Left is MemberExpression leftMember && leftMember.Expression?.NodeType == ExpressionType.Parameter)
                {
                    left = $"{VisitMemberAccess(leftMember)} = '1'";
                }
                else
                {
                    left = VisitExpression(expression.Left);
                }

                if (expression.Right is MemberExpression rightMember && rightMember.Expression?.NodeType == ExpressionType.Parameter)
                {
                    right = $"{VisitMemberAccess(rightMember)} = '1'";
                }
                else
                {
                    right = VisitExpression(expression.Right);
                }
            }
            else
            {
                // 单个表达式
                left = VisitExpression(expression.Left);
                right = VisitExpression(expression.Right);

                if (right == null)
                {
                    // is null 处理
                    return expression.NodeType == ExpressionType.Equal ? $"{left} is null" : $"{left} is not null";
                }

                AddParameter(right, out var paramName);
                return $"{left} {operand} {paramName}";
            }

            return $"{left} {operand} {right}";
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitUnary(UnaryExpression expression)
        {
            switch (expression.NodeType)
            {
                case ExpressionType.Not:
                    var o = VisitExpression(expression.Operand);
                    if (!(o is string))
                    {
                        return !(bool)o;
                    }

                    if (expression.Operand is MemberExpression)
                    {
                        o = $"{o} = '1'";
                    }

                    return $"not ({o})";
                case ExpressionType.Convert:
                    if (expression.Method != null)
                    {
                        return Expression.Lambda(expression).Compile().DynamicInvoke();
                    }
                    break;
            }

            return VisitExpression(expression.Operand);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitMemberAccess(MemberExpression expression)
        {
            if (expression.Expression?.NodeType == ExpressionType.Parameter)
            {
                return MemberToColumn(expression);
            }

            var member = Expression.Convert(expression, typeof(object));
            var lambda = Expression.Lambda<Func<object>>(member);
            var getter = lambda.Compile();
            return getter();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <param name="isUpdate"></param>
        /// <returns></returns>
        protected virtual object VisitMemberInit(MemberInitExpression expression, bool isUpdate = false)
        {
            return string.Join(",", expression.Bindings.Select(x =>
              {
                  if (x.BindingType == MemberBindingType.Assignment)
                  {
                      if (isUpdate)
                      {
                          var sub = ((MemberAssignment)x).Expression;
                          var value = VisitExpression(sub);
                          AddParameter(value, out var paramName);
                          return $" {ResolverHelper.Column((PropertyInfo)x.Member, SqlBuilder)} ={paramName}";
                      }
                      return $"{VisitExpression(((MemberAssignment)x).Expression)}";
                  }
                  return x.Member.ToString();
              }));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual object VisitConstantExpression(ConstantExpression expression) => expression.Value;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        protected virtual string MemberToColumn(MemberExpression expression) =>
            ResolverHelper.Column((PropertyInfo)expression.Member, SqlBuilder);

        /// <summary>
        /// 
        /// </summary>
        /// <param name="expressionType"></param>
        /// <returns></returns>
        protected virtual string GetOperand(ExpressionType expressionType) => expressionType switch
        {
            ExpressionType.Equal => "=",
            ExpressionType.NotEqual => "<>",
            ExpressionType.GreaterThan => ">",
            ExpressionType.GreaterThanOrEqual => ">=",
            ExpressionType.LessThan => "<",
            ExpressionType.LessThanOrEqual => "<=",
            ExpressionType.AndAlso => "and",
            ExpressionType.OrElse => "or",
            ExpressionType.Add => "+",
            ExpressionType.Subtract => "-",
            ExpressionType.Multiply => "*",
            ExpressionType.Divide => "/",
            ExpressionType.Modulo => "MOD",
            ExpressionType.Coalesce => "COALESCE",
            _ => expressionType.ToString(),
        };

        /// <summary>
        /// 
        /// </summary>
        /// <param name="value"></param>
        /// <param name="paramName"></param>
        protected virtual void AddParameter(object value, out string paramName)
        {
            _parameterIndex++;
            paramName = SqlBuilder.PrefixParameter($"p{_parameterIndex}");
            _parameters.Add(paramName, value);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool IsEmpty()
        {
            return _setBuilder.Length == 0 && string.IsNullOrWhiteSpace(_selectQuery);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public string ToSql()
        {
            var set = _setBuilder.ToString();
            var where = _whereBuilder.ToString();
            var orderBy = _orderByBuilder.ToString();
            var query = new StringBuilder();
            if (!string.IsNullOrEmpty(_selectQuery))
            {
                query.Append(_selectQuery);
            }
            if (!string.IsNullOrEmpty(set))
            {
                query.Append(set);
            }
            if (!string.IsNullOrEmpty(where))
            {
                query.Append(where);
            }
            if (!string.IsNullOrEmpty(orderBy))
            {
                query.Append(orderBy);
            }
            if (!string.IsNullOrEmpty(_pagingQuery))
            {
                if (string.IsNullOrEmpty(orderBy))
                {
                    var keyColumns = ResolverHelper.KeyProperties(typeof(TEntity)).Select(p => ResolverHelper.Column(p.Property, SqlBuilder));
                    AppendOrderBy(string.Join(", ", keyColumns), "asc");
                    query.Append(_orderByBuilder.ToString());
                }
                query.Append(_pagingQuery);
            }
            return query.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public string ToSql(out DynamicParameters parameters)
        {
            parameters = _parameters;
            return ToSql();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString() => ToSql();
    }
}
